﻿using System;
using System.Data.SqlServerCe;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OverStore.Configuration;
using OverStore.Exceptions;
using OverStore.Scoping;
using OverStore.Configuration.Components;
using OverStore.Configuration.Builder;
using OverStore.Sql.TableSchema;
using System.Reflection;
using OverStore.Configuration.Builder.Fluent;
using OverStore.Runtime.Storage;
using OverStore.Runtime.ObjectRepository.Mapping;
using OverStore.Runtime.ObjectRepository.CommandSet;
using OverStore.Configuration.ObjectRepository.Depot;

namespace OverStore.TutorialAsTest.Tutorial2.Lesson5
{
    [TestClass]
    public class Lesson5
    {
        /********************
         * We already had considered loading object, reading 
         * object sequenses, and loading collections. We intentionally
         * skip ReloadUsingData method, because it coupled with concept of 
         * object states. So now and here we consider conception of object states,
         * and collection states, and starts learning how to store 
         * object in database (in our case).
         * 
         * When we work with session, we work with memory COPY of data stored in
         * database (or other storage, but for simplicity we will call it database).
         * 
         * In memory each object has only one state: existed. When object is not 
         * required, it will be sweaped by GC after some amount of time, but for 
         * program it does not matter. If program can't reach this object, it does
         * not exist for it.
         * 
         * May be it is surprise, but database works in the same way. Row may exists, or 
         * does not exists. There is no middle state (if does not deep into details, like dirty inter-transaction states).
         * 
         * But OverStore, and most of ORM systems except 'easy-to-implement' 
         * products (wich required base class for entites, collection, or event 
         * for assosiations) should ignore this pretty strategy, and introduce some 
         * strange operations like Delete and Add.
         * 
         * It is different sides of single problem: context syncronization. WTF is context?
         * Context - here - is set of object that served by session. This fact make problem 
         * more complex. We can have ten session in single method and each session may operate 
         * with own database. So, inside session bounds we have SYNCRONIZATION PROBLEM.
         * 
         * Before discussing syncronization problem, lets notice about important fact:
         * in context we usually have not single objects but OBJECT GRAPHS. It means 
         * objects inside context are not independent, they has references to another objects, and this relations 
         * may be simple or complex, depending of requirements.
         * 
         * So, what is the Syncronization Problem? It is common class of tasks require
         * to propagate changes over some boundaries, usually over process boundaries.
         * May be it looks simple, but problem is deeper that seems.
         * For example, following requirements should be satisfied:
         * 1. Transactionality. Syncronization performs as single atomic action,
         *      that should be fully completed or fully rejected. Fortunately, 
         *      main type of storage - database, naturally supports transaction, 
         *      and we will widely use it.
         * 2. Data transformation. Often we need change form of data. It may be easy 
         *      transformation like omitting few columns, put data from column with
         *      name A to property with name B, or change result type for some name.
         *      But we should support more complex tranformation, for example 
         *      converting relational data to hierarchical objects.
         * Syncronization problem may be solved in many ways, but two main 
         * kinds of syncronization strategies are 
         *  * Changes Propagation - only changed data transfers across boundary.
         *  * Data Propagation - all data are transfered across boundary. 
         *  Changes propagation reduces amount of data transfered across boundary,
         * but required additional work to create diffgramm (description of changes).  
         *  Data Propagation does not require additional code to track changes, but 
         * greatly increases amount of data transfered across boundaries.
         * 
         * Different types of storages works better with different syncronization strategy.
         * For example, database required different action for apply different types of 
         * changes (for example, for add new row we should use INSERT INTO command,
         * for delete - DELETE FROM etc). So, for correctly apply changes to database 
         * we should build diffgram for data in any case. For database change propagation 
         * is preferred syncronization strategy. 
         * But if we use xml files and serialization for store data, we havn't STANDARD ways
         * for change data related to single object. In most cases working with xml files 
         * as storage suggests fully rewriting of object graph. In this case we need actual object
         * graph for serialization, so if we have diffgramm, we should apply it. In this case 
         * preffered syncronization strategy is data propagation.
         * 
         * But actually both strategies requires set of object that belong to context. If we create 
         * new instance of object, session has no ways to get informed about this fact. We should explicity 
         * notify session about new object. If we want to delete object, it is not enought to clear
         * all reference to this instance, as is in case with memory. We should explicity call 
         * MarkAsDeleted for this object. So, it is very important to understand: 
         * methods Add() and MarkAsDeleted() of session have no relation to storaging strategy,
         * SQL command that will be called for store object etc. This methods allows link object 
         * to context of specified session, and break this link. 
         * 
         * We should have clear understanding that session can only saves objects are
         * REACHABLE for session. There is two ways for session to reach specified object: explicity
         * point session to object belongs to it, or reach object via OBJECT GRAPH, 
         * for example, due saving collection contains specified object.
         * 
         * If object was created internally inside session, for example using IPersistenceSession.GetByIdentifier(),
         * session saves link to this object. More, if you call one of following methods of session: 
         * ReloadUsingData(instance), Save(instance), MarkAsDeleted(instance) or Add(instance), 
         * session stores link to specified instance, and instance still belongs to session. 
         * But session does not take ownership to parameter argument of following methods:
         * ReloadUsingData(instance, parameter), CreateReader(parameter).
         * 
         * Second way to reach an object is object graph. Object graph consist of references 
         * (other objects which references are holded in object fields (possible exposed via properties or methods).
         * 
         * Object changes.
         * We should differs two types of changes for the object. We can change object
         * data (change result of object's instance name, possible via property or method), 
         * and we can change object relation to context, for example, mark object as deleted, 
         * or add new instance to session.
         * These are two different and independent types of changes. Create or break 
         * object link to context is fundamental type of changing and it does not 
         * depends of type of storaging, syncronization strategy etc. But tracking changes in 
         * object content is aid of optimization, and it is required only for some types of storages.
         * 
         * Persistence session is greedy: it captures all objects thats were ever 'observed'.
         * Each object 'touched' by context still belongs to it. Context never free it till GC collects session. 
         * This means even deleted object participate identity mapping and still accessible by identifier.
         *          
         * In moment when session 'capture' object, it assign identifier for this object. 
         * 
         * There are two major type of identifier generation: session-generated and object-generated. 
         * Values of session-generated keys produced by persistend session, and they are never repeats.
         * 
         * Object-generated identifier are extracted from object data (an instance name, possible via property or method).
         * It means that identifier may match existing primary key. 
         * This means it is possible to delete object without reloading it 
         * from session. But operating with different instances which returns equal values of 
         * identifier may cause IdentityReferenceIntegirtyException, if different instance 
         * with equal key already registered in session.
         * 
         * Next we consider object change states for database storage.
         * There are 4 instance states: 
         *  - U - Unchanged. Object exists in storage and data of object in storage equals
         *          to instance data. No storage operations performs with object due save.
         *  - A - Added. Object does not exist in storage and should be added due save.
         *  - M - Modified. Object exists in storage, but object data is changed, and 
         *          need to be saved in storage.
         *  - D - Deleted. Object exists in storage, but should be deleted from due save.
         *  
         *      Remarks: of course, session can't be sure about actual state of 
         *              storage, but it guess that changes can be applied to storage
         *              correctly. If not, exception will be thrown.
         * Object can be:
         * - C - Captured by session. Object is captured if it was got using 
         *          IPersistenceSession.GetByIdentifier method, or using ExecuteAsReader, or due 
         *          reloading of collection, or if following methods: ReloadUsingData, Save, MarkAsDeleted, Add 
         *          was called using object as argument. 
         * - F - Foreign. Object is created outside of session.
         * 
         * State syntax:
         * {C|F}+{U|A|M|D}[S|I|U|D] => {Captured|Foreign}  + {Unchanged|Added|Modified|Deleted}[Select|Update|Insert|Delete]
         * 
         * Here is table of state transition, describes how session methods change object state.
         * 
         * Action \ State   | C+U           C+A          C+M     C+D        F+U     
         * -----------------|-----------------------------------------------------
         * ReloadUsingData  | C+U[S]        C+U(1)      C+U[S]  C+U[S]      C+U[S](1)
         * Save(All)        | C+U[-]        C+U[I]      C+U[U]  C+U[D]      C+U[I](3)
         * MarkAsDeleted    | C+D[-]        C+A[-]      C+D[-]  C+D[-]      ERR
         * Add              | C+U[-]        C+A[-]      C+M[-]  C+M/U[-]    C+A[-](2)  
         * Get              | C+U[-]        C+A[-]      C+M[-]  C+D[-]      C+U[S]
         * -----------------|------------------------------------------------------
         * make change      | C+M[-]        C+A[-]      C+M[-]  C+D[-]      F+U[-]
         *  -(1) - if object identifier does not match primary key existing 
         *          in storage, the error occurs.
         *  -(2) - if object with such identifier already registered and it is different 
         *          from specified instance, IdentityReferenceIntegrityException will be thrown.
         *  -(3) - if object with such identifier already exists in storage, error will be occured,
         *          because session try to insert record with duplicate primary key.
         *
         * GRAPHomainia. Deep into hierarchies. 
         * We say many times that our objects are arranged into graphs. But we does not set why they do.
         * Why this graphs appears? Is it appears always?
         * If we look inside our database, we detect that all our tables are couple 
         * of fields with simple type of result. It is very important. All fields are simple 
         * values. Event it is xml name. Or user defined type. So, you may ask, what exactly 
         * means 'simple' type. Simple type is type represent isolated part of information that 
         * has sense without additional information required. It means even I have name 'ItemId int not null',
         * which is reference to table Item, result of this name is integer number, at first. And it 
         * has sense without knowledging about table Item. I can sum this result with other number, or
         * I can compare it with other integer.
         * But if we look into some,  even simple, domain model (for example, in Lesson 4), what will we see?
         * There are classes in domain model. And more, that classes has REFERENCES. Reference is simply 
         * result contains address of other object. What this means? This means we need extra information 
         * for correctly construct object with references. For construct row in database table we need 
         * set of simple values (which are INDEPENDENT, and usually has small size). But for construct 
         * object we need set of simple values plus references thats are objects and need to be constructed 
         * in specifal manner. This leads us to uncontrolled and unrestricted growing of data 
         * required for construct object correctly, because we need also reconstruct all its references.
         * 
         * But fortunately we have some technics for avoid load great amount of data from database.
         * 
         * So, we need opportunity to constraint amount of data loaded for construct
         * single object. At first, simply avoid to design domains with rich graphs. Avoid
         * using references only as means for creating self-documented code. References 
         * provides the fastest access, but before reference become accessible for object,
         * it should be constructed and loaded. And event if you use some means like lazy loading, they always 
         * make your program slower. 
         * 
         * Second means we're already touched is lazy loading. Lazy loading is technique allows 
         * loading content of object when you actually access it but not early. Problem with lazy 
         * loading is hardness of implementation. For implement lazy loading we should 
         * catch moment just before accessing object content and notify session 
         * about this. It requires a lot of hand labor coding if we use .NET Compact Framework, 
         * or we can use some of facilities provided by dynamic code generation, if we use full
         * 'adult' .NET framework, the third opportunity is using of complile time code generation. 
         * But unfortunately there is no way to make lazy loading transparent for all .NET Frameworks in all cases
         * without any additional work.
         * 
         * Let imagine simple object graph existed in session
         * 
         * Schema 1. Example of object graph in session.
         * ---------------------------
         * A1 <--- B1 <--- C1 <-- B3
         *    |--- B2 <--- C2
         * 
         * A2 <--- B1 <--- D1
         *    |--- B3 <--- D2..D8  
         *    
         * C3 <--- B4 <--- D10 
         * E1 <--- G2 .. G23
         * 
         * The instance A1 of type A contains reference to instance B1 (of type B) and B2 
         * (into different fields).
         * Instance B3 contains reference to collection of instances { D2, D3, D4 .. D8 }.
         * Lets remember term REACHABILITY for session. Session can reach objects navigate 
         * throught references, but not backward (it is impossible to get all object are referenced specified object). 
         * For reach some object, session requires the start point - object which references session starts to explore. 
         * For example, starting from object B12 session can reach object C2, from D1 session can 
         * reach nothing, from A1 session can reach B1, C1, B3, B2, C2, D1, D2 .. D8 - almost all
         * object graph excluding A2. It means that during, for example, saving object A1, session should save 
         * all these objects. Now we skip deep considering of reference configuration, but says it is 
         * possible to flexible define how references would be processed, would reference be loaded, saved, deleted
         * with object etc.
         * 
         * Unit Of Work.
         * After discussing object graphs, let look again on Schema 1. All these objects are captured by session and session
         * can reach it.
         * So, session can save all changed objects contains inside it. This scenario called Unit Of Work. 
         * Calling SaveAll() method of session causes saving changes of all object registered in session.
         * 
         ********************/

        /*******
         * Finally, our test data. table PlaceType:
         * OverrideDbType Name            Description
         * -------------------------------------------------------------------------------------------------------
         * 1	Purse	        Place to store money: card, bank account, purse itself.
         * 2	Account	        Account to pay to or get payment from: bank account, booking, bank terminal.
         * 3	Warehouse	    Place to store products: warehouse itself, home. Product can be placed in and got from at any time.
         * 4	Shop	        Place to buy or sale product. Also for recycle old or bad products. 
         *******/

        class PlaceType
        {
            public int Type { get; set; }

            public string Name { get; set; }

            public string Description { get; set; }
        }

        static IComponentConfiguration<IMapper<PlaceType>> CreatePlaceTypeMapper()
        {
            return Component.Mapper.Delegate<PlaceType>(
                (item, name, p) =>
                {
                    if (name == "Type")
                        return item.Type;
                    if (name == "Name")
                        return item.Name;
                    if (name == "Description")
                        return item.Description;
                    if (name == "orig__Type")
                        return p.GetIdentifier<PlaceType, int>(item);
                    return Missing.Value;
                },
                (data, item, p) =>
                {
                    item.Name = data.GetString("Name");
                    item.Description = data.GetString("Description");
                });
        }

        static IComponentConfiguration<ICommandSet> CreatePlaceTypeCommandSet()
        {
            return Component.CommandSet.FromTable(
                    Component.CreateTable("PlaceType").Key<int>("Type").Column<String>("Name").Column<String>("Description"), false);
        }

        static OverStoreConfiguration CreatePlaceTypeConfigWithAttachedKey(string connectionString, int lastIdentifier)
        {
            return OverStoreFluent.Configure()
                .LogToConsole().ConnectToDatabase<SqlCeConnection>(connectionString, ProcessOpenedReadersMode.ForciblyCloseReaders)
                .CustomClassRepository<PlaceType, int>()
                    .WithDepot(new DelegateDepotConfiguration<PlaceType, int>
                                {
                                    // Here we intentionally define ApplyIdentifier delegate
                                    // for show how session supports constantness for identifiers.
                                    StoreIdentifierCopy = (instance, identifier) => instance.Type = identifier,
                                    CreateInstanceFunction = () => new PlaceType(),
                                    IdentifierGenerator = Component.IdentifierGenerator.Delegate<PlaceType, int>((p, last) => p.Type = last = last + 1, lastIdentifier),
                                })
                    .WithMapper(CreatePlaceTypeMapper())
                    .WithCommands(CreatePlaceTypeCommandSet())
                    // We need specify custom changes tracker, because track changes using 
                    // object mapping result does not recognize identifier changes.
                    .WithChangesTracker(Component.InstanceChangesTracker.Delegate<PlaceType, int, int>(i => i.Type + (i.Name ?? String.Empty).GetHashCode() + (i.Description ?? String.Empty).GetHashCode()))
                    .ByIdentifierAttaching(i => i.Type)
                .EndObjectRepository()
             .End();
        }

        static OverStoreConfiguration CreatePlaceTypeConfigWithInnerKey(string connectionString)
        {
            return OverStoreFluent.Configure()
                .LogToConsole().ConnectToDatabase<SqlCeConnection>(connectionString, ProcessOpenedReadersMode.ForciblyCloseReaders)
                .CustomClassRepository<PlaceType, int>()
                    .WithDepot(new DelegateDepotConfiguration<PlaceType, int>
                    {
                        // Here we intentionally define ApplyIdentifier delegate
                        // for show how session supports constantness for identifiers.
                        StoreIdentifierCopy = (instance, identifier) => instance.Type = identifier,
                        CreateInstanceFunction = () => new PlaceType(),
                        IdentifierGenerator = Component.IdentifierGenerator.Delegate<PlaceType, int>((instance, lastId) => instance.Type, 1000)
                    })
                    .WithMapper(CreatePlaceTypeMapper())
                    .WithCommands(CreatePlaceTypeCommandSet())
                // We need specify custom changes tracker, because track changes using 
                // object mapping result does not recognize identifier changes.
                    .WithChangesTracker(Component.InstanceChangesTracker.Delegate<PlaceType, int, int>(i => i.Type + (i.Name ?? String.Empty).GetHashCode() + (i.Description ?? String.Empty).GetHashCode()))
                    .ByIdentifierAttaching(i => i.Type)
                .EndObjectRepository()
             .End();
        }

        [TestMethod]
        public void SaveObjectSimple()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            PlaceType shop = null;
            using (var session = CreatePlaceTypeConfigWithAttachedKey(connectionString, 1000).CreateSession())
            {
                shop = session.GetByIdentifier<PlaceType, int>(4);
                Assert.AreEqual("Place to buy or sale product. Also for recycle old or bad products.", shop.Description.Trim());

                shop.Description = "Shop is place where you can by or sale product. May be real or virtual.";
                session.Save(shop);
            }

            using (var session2 = CreatePlaceTypeConfigWithAttachedKey(connectionString, 1000).CreateSession())
            {
                var shop2 = session2.GetByIdentifier<PlaceType, int>(4);
                Assert.AreEqual(shop.Description, shop2.Description);

                shop2.Description = "Place to buy or sale product. Also for recycle old or bad products.";
                session2.Save(shop2);
            }
        }

        /// <summary>
        /// Identifiers of object are constant, usually. 
        /// Even if identifier result stores in object data (as property, for example),
        /// session update its result on save.
        /// 
        /// We should specify custom delegate based change tracker because standard
        /// object-data based changes tracker, which uses mapping result data for tracking,
        /// does not recognize changes in primary keys.
        /// </summary>
        [TestMethod]
        public void SaveObjectConstantKey()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreatePlaceTypeConfigWithAttachedKey(connectionString, 1000).CreateSession())
            {
                var shop = session.GetByIdentifier<PlaceType, int>(4);
                shop.Type = 9;
                session.Save<PlaceType>(shop);
                var shop2 = session.GetByIdentifier<PlaceType, int>(4);
                Assert.AreSame(shop, shop2);
                Assert.AreEqual(4, shop2.Type);
            }
        }

        [TestMethod]
        public void SaveObjectNew()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            var pt = new PlaceType
            {
                Name = "Garson",
                Description = "Person that provide abstract service."
            };
            using (var session = CreatePlaceTypeConfigWithAttachedKey(connectionString, 1000).CreateSession())
            {
                session.Save<PlaceType>(pt);
                Assert.AreEqual(1001, pt.Type);
            }
            var newPtId = 1001; // Last identifier = 1000, so next identifer should be 1001
            // note that PlaceType.Type 

            using (var session2 = CreatePlaceTypeConfigWithAttachedKey(connectionString, 1000).CreateSession())
            {
                var pt2 = session2.GetByIdentifier<PlaceType, int>(newPtId);

                Assert.AreEqual(pt.Description, pt2.Description);
                Assert.AreEqual(pt.Name, pt2.Name);
                Assert.AreEqual(pt.Type, pt2.Type);
                Assert.AreEqual(newPtId, pt2.Type);

                session2.MarkAsDeleted(pt2);
                session2.Save(pt2);
            }
        }

        [TestMethod]
        [ExpectedException(typeof(PersistenceOperationFailedException))]
        public void SaveObjectDeleted()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreatePlaceTypeConfigWithAttachedKey(connectionString, 1000).CreateSession())
            {
                var pt = new PlaceType
                {
                    Name = "For delete",
                    Description = "At the end of the test this value should be deleted"
                };
                session.Save<PlaceType>(pt);

                Assert.AreEqual(1001, pt.Type);

                session.MarkAsDeleted(pt);
                session.Save(pt);
                // We intend that object take identifier 1001 = last_id + 1.
                // Object should be accessible and same as deleted.
                var pt2 = session.GetByIdentifier<PlaceType, int>(1001);
                Assert.AreSame(pt2, pt);
            }
            // In new session object should not be accessible, because it does not 
            // exist in storage.
            using (var session2 = CreatePlaceTypeConfigWithAttachedKey(connectionString, 1000).CreateSession())
            {
                session2.GetByIdentifier<PlaceType, int>(1001);
            }
        }

        [TestMethod]
        public void SaveObjectErrorNotRegisteredInSession()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreatePlaceTypeConfigWithInnerKey(connectionString).CreateSession())
            {
                var pt = new PlaceType
                {
                    Type = 1,
                    Name = "For delete",
                    Description = "At the end of the test this value should be deleted"
                };
                try
                {
                    session.Save<PlaceType>(pt);
                    Assert.Fail("Inserting successful, test hasn't more sense");
                }
                catch (PersistenceOperationFailedException)
                {
                    // It is ok, there is place type with id = 1 in database
                }

                Assert.AreNotSame(pt, session.GetByIdentifierOrDefault<PlaceType, int>(1));


            }
        }

        [TestMethod]
        public void ContextBinding_OofC_U_Reload()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreatePlaceTypeConfigWithInnerKey(connectionString).CreateSession())
            {
                var placeType = new PlaceType { Type = 1 };
                Assert.IsTrue(session.Reload(placeType));
                Assert.AreEqual(1, placeType.Type);
                Assert.AreEqual("Purse", placeType.Name.Trim());
                Assert.AreEqual("Place to store money: card, bank account, purse itself.", placeType.Description.Trim());

                Assert.AreSame(placeType, session.GetByIdentifier<PlaceType, int>(1));
            }
        }

        [TestMethod]
        public void ContextBinding_OofC_U_ReloadNonExisting()
        {
            var cs = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreatePlaceTypeConfigWithInnerKey(cs).CreateSession())
            {
                var placeType = new PlaceType { Type = 1000 };
                Assert.IsFalse(session.Reload(placeType));
                Assert.IsNull(session.GetByIdentifierOrDefault<PlaceType, int>(1000));
            }
        }

        [TestMethod]
        public void ContextBinding_OofC_U_Reload_AttachedKey()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreatePlaceTypeConfigWithAttachedKey(connectionString, 11000).CreateSession())
            {
                var placeType = new PlaceType { Type = 1 };
                Assert.IsFalse(session.Reload(placeType));
            }
        }

        [TestMethod]
        [ExpectedException(typeof(PersistenceOperationFailedException))]
        public void ContextBinding_OofC_U_Save()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreatePlaceTypeConfigWithInnerKey(connectionString).CreateSession())
            {
                var placeType = new PlaceType { Type = 1, Name = "Purse", Description = "Purse" };
                session.Save<PlaceType>(placeType);

                using (var session2 = CreatePlaceTypeConfigWithInnerKey(connectionString).CreateSession())
                {
                    var placeType2 = session2.GetByIdentifier<PlaceType, int>(1);

                    Assert.AreNotSame(session, session2);
                    Assert.AreEqual("Purse", placeType2.Description);

                    placeType.Description = "Place to store money: card, bank account, purse itself.";
                }
            }
        }

        [TestMethod]
        [ExpectedException(typeof(IdentityReferenceIntegrityException))]
        public void ContextBinding_OofC_U_Save_GetBefore()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreatePlaceTypeConfigWithInnerKey(connectionString).CreateSession())
            {
                var placeType1 = session.GetByIdentifier<PlaceType, int>(1);
                var placeType2 = new PlaceType { Type = 1, Name = "Purse", Description = "purse" };
                session.Save<PlaceType>(placeType2);
            }
        }

        [TestMethod]
        public void ContextBinding_OofC_U_Save_AttachedKeys()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreatePlaceTypeConfigWithAttachedKey(connectionString, 400).CreateSession())
            {
                var placeType = new PlaceType { Type = 1, Name = "PurseChanged", Description = "PurseChanged" };
                session.Save(placeType);

                Assert.AreSame(placeType, session.GetByIdentifier<PlaceType, int>(401));
                Assert.AreNotSame(placeType, session.GetByIdentifier<PlaceType, int>(1));
            }
            using (var session2 = CreatePlaceTypeConfigWithAttachedKey(connectionString, 500).CreateSession())
            {
                var placeType2 = session2.GetByIdentifier<PlaceType, int>(401);
                Assert.AreEqual("PurseChanged", placeType2.Name);
                session2.MarkAsDeleted(placeType2);
                session2.Save(placeType2);
            }
        }

        /// <summary>
        /// This test should throw PersistenceOperationFailedException because 
        /// calling <see cref="IPersistenceSession.MarkAsDeleted"/> method 
        /// on object that is in session before cause to add this object as 
        /// new and generate identifier for it. Then due calling <see cref="IPersistenceSession.Save"/>
        /// method, session trys to delete object with just generated identifier, so it should cause an error.
        /// </summary>
        [TestMethod]
        [ExpectedException(typeof(PersistenceOperationFailedException))]
        public void ContextBinding_OofC_Delete()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            using (var session = CreatePlaceTypeConfigWithAttachedKey(connectionString, 1000).CreateSession())
            {
                var placeType = new PlaceType { Type = 1, Name = "PurseChanged", Description = "PurseChanged" };
                session.MarkAsDeleted(placeType);

                session.Save(placeType);

                Assert.AreSame(placeType, session.GetByIdentifier<PlaceType, int>(1001));
            }
        }

        [TestMethod]
        public void ContextBinding_OofC_Delete_InnerKeys()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            var placeType = new PlaceType { Type = 1500, Name = "Test" };
            // prepare for delete test: create new instance.
            using (var session = CreatePlaceTypeConfigWithInnerKey(connectionString).CreateSession())
            {
                session.Save(placeType);
            }

            // check object correctly added.
            using (var sessionN = CreatePlaceTypeConfigWithInnerKey(connectionString).CreateSession())
            {
                var placeTypeTest = sessionN.GetByIdentifier<PlaceType, int>(1500);
                Assert.AreEqual(1500, placeType.Type);
                Assert.AreEqual("Test", placeType.Name);
                Assert.IsNull(placeType.Description);
            }

            // delete object in another session. Object not registered in session
            // before deleting. Must throws exception
            using (var session2 = CreatePlaceTypeConfigWithInnerKey(connectionString).CreateSession())
            {
                var placeType2 = new PlaceType { Type = 1500, Name = "OtherTest" };
                try
                {
                    session2.MarkAsDeleted(placeType2);
                    session2.Save(placeType2);
                    Assert.Fail("Must throw exception on deleting entity was not registered in session.");
                }
                catch (PersistenceOperationFailedException)
                {
                    // It is ok.
                }
            }

            // Delete 
            using (var session = CreatePlaceTypeConfigWithInnerKey(connectionString).CreateSession())
            {
                var placeType5 = session.GetByIdentifier<PlaceType, int>(1500);
                session.MarkAsDeleted(placeType5);
                session.Save(placeType5);
            }

            using (var session3 = CreatePlaceTypeConfigWithInnerKey(connectionString).CreateSession())
            {
                try
                {
                    var placeType3 = session3.GetByIdentifier<PlaceType, int>(1500);
                    Assert.Fail("Detached object with inner key is not deleted correctly.");
                }
                catch (PersistenceOperationFailedException)
                {
                    // do nothing, exception is expected behavior.
                }
            }
        }

        [TestMethod]
        public void ContextBinding_BtoC_DeleteReload()
        {
            var connectionString = TestHelper.CreateMyMoneyCopyAndBuildConnectionString();
            PlaceType pt = null;
            using (var session = CreatePlaceTypeConfigWithAttachedKey(connectionString, 1010).CreateSession())
            {
                pt = session.GetByIdentifier<PlaceType, int>(1);
                session.MarkAsDeleted(pt);
                session.Reload(pt);
                session.Save(pt);
            }

            using (var session2 = CreatePlaceTypeConfigWithAttachedKey(connectionString, 1010).CreateSession())
            {
                var pt2 = session2.GetByIdentifier<PlaceType, int>(1);
                Assert.IsNotNull(pt2);
                Assert.AreEqual(pt.Type, pt2.Type);
            }
        }
    }
}
